
import os, sys, traceback
from NBT import NBT

ChunkXSize = 16
ChunkYSize = 128
ChunkZSize = 16

import functools

class memoize(object):
    def __init__(self, func):
        self.func = func
        self.memoized = {}
        self.method_cache = {}
    def __call__(self, *args):
        return self.cache_get(self.memoized, args,
            lambda: self.func(*args))
    def __get__(self, obj, objtype):
        return self.cache_get(self.method_cache, obj,
            lambda: self.__class__(functools.partial(self.func, obj)))
    def cache_get(self, cache, key, func):
        try:
            return cache[key]
        except KeyError: 
            cache[key] = func()
            return cache[key]


def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
    '''
    Convert positive integer to a base36 string.
    '''
    if not isinstance(number, (int, long)):
        number = int(number)
    neg = number < 0
    if neg:
        number *= -1
 
    # Special case for zero
    if number == 0:
        return '0'
 
    base36 = ''
    while number != 0:
        number, i = divmod(number, 36)
        base36 = alphabet[i] + base36
 
    if neg:
        base36 = '-' + base36
    return base36
 
def base36decode(number):
    return int(number,36)
 

class MinecraftBlockType:
    def __init__(self, id, name, scale, coor):
        self.id = id
        self.name = name
        self.sca = scale
        self.coor = coor

# Information about a minecraft block.
class MinecraftBlock:
    types = {}
    name_to_id = {}
    def __init__(self, id, skyLight = 0, blockLight = 0):
        # The type of this block.
        self.id = ord(id)
        # How much light the block receives from the sun.
        self.skyLight = skyLight
        # How much light the block receives from lights.
        self.blockLight = blockLight

    @classmethod
    def setup_types(cls):
        # The "Unknown" items aren't REALLY unknown.. just laziness..
        # http://minecraftwiki.net/wiki/Data_Values
        cls.add(0, "Space", scale = (0.8, 0.8, 0.8), coor = ((1, 2), (1, 2)))
        cls.add(1, "Stone", coor = ((1, 15), (1, 15)))
        cls.add(2, "Grass", coor = ((3, 15), (0, 15)))
        cls.add(3, "Dirt", scale = (0.2, 0.2, 0.2), coor = ((2, 15), (2, 15)))
        cls.add(4, "Cobblestone", coor = ((0, 14), (0, 14)))
        cls.add(5, "Wood", coor = ((4, 15), (4, 15)))
        cls.add(6, "Sapling", coor = ((15, 15), (15, 15)))
        cls.add(7, "Bedrock", coor = ((5, 13), (5, 13)))
        cls.add(8, "Water", coor = ((14, 3), (14, 3)))
        cls.add(9, "Stationary Water", coor = ((14, 3), (14, 3)))
        cls.add(10, "Lava", coor = ((14, 0), (14, 0)))

        cls.add(11, "Stationary Lava", coor = ((14, 0), (14, 0)))
        cls.add(12, "Sand", coor = ((2, 14), (2, 14)))
        cls.add(13, "Gravel", scale = (0.2, 0.2, 0.2), coor = ((3, 14), (3, 14)))
        cls.add(14, "Gold Ore", coor = ((0, 13), (0, 13)))
        cls.add(15, "Iron Ore", coor = ((1, 13), (1, 13)))
        cls.add(16, "Coal Ore", coor = ((2, 13), (2, 13)))
        cls.add(17, "Log", coor = ((4, 14), (5, 14)))
        cls.add(18, "Leaves", coor = ((4, 12), (4, 12)))
        cls.add(19, "Unknown")
        cls.add(20, "Glass", coor = ((1, 12), (1, 12)))

        cls.add(21, "Unknown")
        cls.add(22, "Unknown")
        cls.add(23, "Unknown")
        cls.add(24, "Unknown")
        cls.add(25, "Unknown")
        cls.add(26, "Unknown")
        cls.add(27, "Unknown")
        cls.add(28, "Unknown")
        cls.add(29, "Unknown")
        cls.add(30, "Unknown")

        cls.add(31, "Unknown")
        cls.add(32, "Unknown")
        cls.add(33, "Unknown")
        cls.add(34, "Unknown")
        cls.add(35, "Unknown")
        cls.add(36, "Unknown")
        cls.add(37, "Unknown")
        cls.add(38, "Unknown")
        cls.add(39, "Unknown")
        cls.add(40, "Unknown")

        cls.add(41, "Unknown")
        cls.add(42, "Unknown")
        cls.add(43, "Unknown")
        cls.add(44, "Unknown")
        cls.add(45, "Unknown")
        cls.add(46, "Unknown")
        cls.add(47, "Unknown")
        cls.add(48, "Mossy Cobblestone", coor = ((4, 12), (4, 12)))
        cls.add(49, "Obsidian", coor = ((5, 13), (5, 13)))
        cls.add(50, "Torch", scale = (0.3, 0.8, 0.3), coor = ((0, 10), (0, 10)))

        cls.add(51, "Unknown")
        cls.add(52, "Mob Spawner", coor = ((1, 11), (1, 11)))
        cls.add(53, "Unknown")
        cls.add(54, "Chest", coor = ((10, 14), (9, 14)))
        cls.add(55, "Redstone Wire", scale = (0.6, 0.05, 0.6), coor = ((4, 10), (4, 10)))
        cls.add(56, "Diamond Ore", coor = ((2, 12), (2, 12)))
        cls.add(57, "Unknown")
        cls.add(58, "Workbench", coor = ((10, 14), (11, 13)))
        cls.add(59, "Unknown")
        cls.add(60, "Unknown")

        cls.add(61, "Furnace (inactive)")
        cls.add(62, "Furnace (active)")
        cls.add(63, "Unknown")
        cls.add(64, "Unknown")
        cls.add(65, "Unknown")
        cls.add(66, "Minecart Tracks", scale = (1.0, 0.05, 1.0), coor = ((0, 7), (0, 7)))
        cls.add(67, "Unknown")
        cls.add(68, "Unknown")
        cls.add(69, "Unknown")
        cls.add(70, "Unknown")

        cls.add(71, "Unknown")
        cls.add(72, "Unknown")
        cls.add(73, "Redstone Ore", coor = ((3, 12), (3, 12)))
        cls.add(74, "Glowing Redstone Ore", coor = ((3, 12), (3, 12)))
        cls.add(75, "Redstone Torch (off)", scale = (0.3, 0.8, 0.3), coor = ((3, 8), (3, 8)))
        cls.add(76, "Redstone Torch (on)", scale = (0.3, 0.8, 0.3), coor = ((3, 9), (3, 9)))
        cls.add(77, "Unknown")
        cls.add(78, "Snow", scale = (1.0, 0.05, 1.0), coor = ((2, 11), (2, 11)))
        cls.add(79, "Unknown")
        cls.add(80, "Unknown")

        cls.add(81, "Cactus", coor = ((6, 12), (5, 12)))
        cls.add(82, "Unknown")
        cls.add(83, "Unknown")
        cls.add(84, "Unknown")
        cls.add(85, "Unknown")

    @classmethod
    # These default texture coordinates produce an ugly block. Motivation to fix it.
    def add(cls, id, name, scale = (1.0, 1.0, 1.0), coor = ((1.5, 14.5), (1.5, 14.5))):
        cls.types[id] = MinecraftBlockType(id, name, scale, coor)
        cls.name_to_id[name] = id

@memoize
def coord_to_index(x, y, z):
    return int(y) + (int(z) * ChunkYSize) + (int(x) * ChunkYSize * ChunkZSize)


class Chunk:
    def __init__(self, blocks, blockLight, skyLight):
        self.blocks = blocks
        self.blockLight = blockLight
        self.skyLight = skyLight

    @memoize
    def Lookup(self, x, y, z):
        ind = coord_to_index(x, y, z)
        return MinecraftBlock(self.blocks[ind], blockLight = self.LookupLight(ind))

    @memoize
    def LookupLight(self, ind):
        # this currently only computes the light emitted from other blocks, but not the sun
        n = ind / 2
        shift = 4
        mask = 0xf0
        if ind & 0x1: #odd
            mask = 0x0f
            shift = 0
        return (ord(self.blockLight[n]) & int(mask)) >> shift

# An alpha level from minecraft. The shape gives the minecraft block id's for the blocks
# at particular locations. Gives -1 for regions without data. minecraft levels
# are oriented with the Y axis going up and down.

class MinecraftLevel:
    def __init__(self, path):
        self.source = path
        self.chunks = {}
    # Represents a chunk of data in the minecraft format.
    # Tries loading the chunk at the specified position. This is done automatically
    # when an area is requested.
    def LoadChunk(self, x, y):
        if (x, y) in self.chunks:
            return self.chunks[(x, y)]
        # Get chunk name
        folda = base36encode(((int(x) % 64) + 64) % 64)
        foldb = base36encode(((int(y) % 64) + 64) % 64)
        f = os.path.join(self.source, folda, foldb, "c.%s.%s.dat" % (base36encode(x), base36encode(y)))
        #print "Trying to open path", f
        chunk = None
        if os.path.exists(f):
            try:
                dat = NBT(f).Read()
                #print dat, dat.name, dat.data
                level = dat.data.data["Level"].data
                self.chunks[(x, y)] = chunk = Chunk(level.data["Blocks"].data.data
                                                ,level.data["BlockLight"].data.data
                                                ,level.data["SkyLight"].data.data)
            except:
                print "There was an exception processing chunk", f
                traceback.print_exc(file=sys.stdout)
                self.chunks[(x, y)] = chunk = None
        else:
            print "Chunk", f, "can't be found.."
            self.chunks[(x, y)] = chunk = None
        return chunk

    # Unloads a chunk from memory.
    def UnloadChunk(self, pos):
        del self.chunks[pos]

    def Lookup(self, loc):
        if loc.y < 0 or loc.y >= ChunkYSize:
            return None
        else:
            chunk = self.LoadChunk(loc.x / ChunkXSize, loc.z / ChunkZSize)
            if not chunk:
                return None
            else:
                return chunk.Lookup((loc.x % ChunkXSize + ChunkXSize) % ChunkXSize,
                                    loc.y,
                                    (loc.z % ChunkZSize + ChunkZSize) % ChunkZSize);

if __name__ == "__main__":
    MinecraftBlock.setup_types()

    ml = MinecraftLevel("World1")
    c = ml.LoadChunk(0, 0)
    print dir(c)

